/*
 * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.enterprise.tools.visualizer.hk2;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * reads a wires XML file generated by the dependency-verifier and outputs a dot
 * file (Use <a href="http://www.graphviz.org/">GraphViz</a> to create images
 * from the dot file) that helps visualizing the wiring dependencies between
 * packages. The tool also supports viewing the dependencies of only a subset of
 * the packages.
 * 
 * @author Sivakumar Thyagarajan
 */
public class DotGenerator {
	private final static boolean DEBUG = false;

	PrintStream wireOut = null;
	GeneratorOptions options = null;

	/**
	 * An internal class holding all the command line options
	 * supported by this DotGenerator
	 * 
	 * @author Sivakumar Thyagarajan
	 */
	static class GeneratorOptions {
		// Accept XML files generated by HK2 dependency-verifier
		@Option(name = "-i", usage = "Input Wires XML file")
		public String input = "wires.xml";

		@Option(name = "-o", usage = "Output DOT file", required = true)
		public String output;

		@Option(name = "-m", usage = "Show only packages that contains the specified substring")
		public String match = "";// By default, match all.

		// receives other command line parameters than options
		@Argument
		public List<String> arguments = new ArrayList<String>();
	}
	
	/**
	 * A simple class representing all the information about a
	 * package that can be derived from the wires.xml
	 * 
	 * @author Sivakumar Thyagarajan
	 */
	class PackageInfo {
		String packageName, exportedBy = null;
		String[] importedBy = null;

		public PackageInfo(String packageName, String exportedBy,
				String[] importedBy) {
			this.packageName = packageName;
			this.exportedBy = exportedBy;
			this.importedBy = importedBy;
		}
	}
	
	public DotGenerator(GeneratorOptions go) throws Exception {
		this.options = go;
		wireOut = new PrintStream(new FileOutputStream(this.options.output));
		initXML();
		generate();
	}

	private Document doc = null;

	private void initXML() throws Exception {
		File file = new File(this.options.input);
		debug("file " + file);
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
        try {
            dbf.setFeature(FEATURE, true);
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException("ParserConfigurationException was thrown. The feature '"
                + FEATURE + "' is not supported by your XML processor.", e);
        }
		DocumentBuilder db = dbf.newDocumentBuilder();
		doc = db.parse(file);
		doc.getDocumentElement().normalize();
	}

	private PackageInfo[] getPackageXML() throws IOException {
		NodeList pkgLst = doc.getElementsByTagName("Package");
		debug("Package count:" + pkgLst.getLength());
		List<PackageInfo> pkgInfos = new ArrayList<PackageInfo>();
		for (int i = 0; i < pkgLst.getLength(); i++) {
			Node pkgNode = pkgLst.item(i);
			if (pkgNode.getNodeType() == Node.ELEMENT_NODE) {
				Element packageElement = (Element) pkgNode;
				// exports
				NodeList ExportersList = packageElement
						.getElementsByTagName("Exporters");
				Element exporterElt = (Element) ExportersList.item(0);
				String exporter = ((Node) exporterElt.getChildNodes().item(0))
						.getNodeValue().trim();
				debug("Exporter : " + exporter);

				// importers
				NodeList importersList = packageElement
						.getElementsByTagName("Importers");
				Element importerElt = (Element) importersList.item(0);
				String importers = ((Node) importerElt.getChildNodes().item(0))
						.getNodeValue().trim();
				debug("Importers : " + importers);

				// Get package name and return PackageInfos
				String pkgName = packageElement.getAttribute("name").trim();
				debug("Package Name : " + pkgName);
				PackageInfo pkgInfo = new PackageInfo(pkgName, exporter,
						split(importers));
				pkgInfos.add(pkgInfo);
			}
		}
		return pkgInfos.toArray(new PackageInfo[] {});
	}

	private void generate() throws Exception {
		generateDotStart();
		PackageInfo[] pkgInfos = getPackageXML();
		for (PackageInfo pkgInfo : pkgInfos) {
			// Match if needed
			String matchString = this.options.match.trim();
			boolean matchNeeded = !matchString.isEmpty();
			if (matchNeeded) {
				if (pkgInfo.exportedBy.contains(matchString)) {
					generateDotEdge(pkgInfo.importedBy, pkgInfo.exportedBy,
							pkgInfo.packageName);
				}
			} else {
				generateDotEdge(pkgInfo.importedBy, pkgInfo.exportedBy,
						pkgInfo.packageName);
			}
		}
		generateDotEnd();
	}

	private void debug(String s) {
		if (DEBUG)
			System.err.println(s);
	}

	private void debug(String text, String s) {
		debug(text, new String[] { s });
	}

	private void debug(String text, String[] arr) {
		StringBuffer sb = new StringBuffer(text);
		for (String s : arr) {
			sb.append(s).append(" , ");
		}
		debug(sb.toString());
	}

	public static void main(String[] args) throws Exception {
		DotGenerator.GeneratorOptions options = new DotGenerator.GeneratorOptions();
		CmdLineParser parser = new CmdLineParser(options);
		try {
			parser.parseArgument(args);
		} catch (CmdLineException e) {
			System.err.println(e.getMessage());
			System.err.println("java -jar program-name.jar [options...] arguments...");
			parser.printUsage(System.err);
			return;
		}
		new DotGenerator(options);
	}

	// Generate the beginning of the dot file
	private void generateDotStart() {
		this.wireOut.println("digraph  wiring {");
		this.wireOut.println("node [color=grey, style=filled];");
		this.wireOut.println("node [fontname=\"Verdana\", size=\"30,30\"];");
		String date = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
				DateFormat.SHORT).format(new Date());
		StringBuffer footer = new StringBuffer();
		footer.append("graph [ fontname = \"Arial\", fontsize = 26,style = \"bold\", ");
		footer.append("label = \"\\nGlassFish v3 OSGi bundle wiring relationship diagram");
		if (!this.options.match.trim().isEmpty()) {
			footer.append("\\n Filter: " + this.options.match.trim()
					+ " bundles");
		}
		footer.append("\\nSun Microsystems");
		footer.append("\\n\\nDate: " + date + "\\n\", "
				+ "ssize = \"30,60\" ];");
		this.wireOut.println(footer.toString());
	}

	// Generate a Dot representation for each edge in the graph
	private void generateDotEdge(String[] importedBy, String exportedBy,
			String pkg) {
		if (importedBy.length == 0)
			return;
		for (String s : importedBy) {
			if (!s.equals(exportedBy)) { // remove self-loops for readability
				this.wireOut.println("\"" + s + "\" -> \"" + exportedBy
						+ "\" [label =\"" + pkg + "\"" + "]");
			}
		}
	}

	// End the dot file generation
	private void generateDotEnd() {
		this.wireOut.println("}");
	}

	// Utility class to split the importers representation (space separated) 
	//in wires.xml
	private String[] split(String s) {
		StringTokenizer st = new StringTokenizer(s);
		List<String> l = new ArrayList<String>();
		while (st.hasMoreTokens()) {
			l.add(st.nextToken());
		}
		return l.toArray(new String[] {});
	}
}
